package com.chenguo.aspect;


import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;


@Aspect //切面类
@Component //组件
public class WebLogAspect {


    private Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    /**
     * 实现web层的日志切面
     * 定义切入点，切入点为com.chenguo.aspect下的所有函数
     *   1）execution(* *(..))
     *   //表示匹配所有方法
     *   2）execution(public * cn.luischen.controller.UserController.*(..))
     *   //表示匹配cn.luischen.controller.UserController中所有的公有方法
     *   3）execution(* cn.luischen.controller..*.*(..))
     *   //表示匹配cn.luischen.controller包及其子包下的所有方法
     */
    @Pointcut("execution(public * com.chenguo.controller" +
            "..*.*(..))")
    public void pointCut(){}
    /*-------------------------------------------------------------------------------------------------*/
    /**
     * 前置通知，方法调用前被调用
     * @param joinPoint/null
     */
    //@Before(value = "pointCut()")
 /*   注意：这里用到了JoinPoint和RequestContextHolder。
            1）、通过JoinPoint可以获得通知的签名信息，如目标方法名、目标方法参数信息等；
            2）、通过RequestContextHolder来获取请求信息，Session信息；*/
    public void before(JoinPoint joinPoint){
        logger.info("前置通知");
        //获取目标方法的参数信息
        Object[] obj = joinPoint.getArgs();
        //AOP代理类的信息
        joinPoint.getThis();
        //代理的目标对象
        joinPoint.getTarget();
        //用的最多 通知的签名
        Signature signature = joinPoint.getSignature();
        //代理的是哪一个方法
        logger.info("代理的是哪一个方法"+signature.getName());
        //AOP代理类的名字
        logger.info("AOP代理类的名字"+signature.getDeclaringTypeName());
        //AOP代理类的类（class）信息
        signature.getDeclaringType();
        //获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //如果要获取Session信息的话，可以这样写：
        //HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
        //获取请求参数
        Enumeration<String> enumeration = request.getParameterNames();
        Map<String,Object> parameterMap = new HashMap<String,Object>();
        while (enumeration.hasMoreElements()){
            String parameter = enumeration.nextElement();
            parameterMap.put(parameter,request.getParameter(parameter));
        }
        String str = JSON.toJSONString(parameterMap);
        if(obj.length > 0) {
            logger.info("请求的参数信息为："+str);
        }
        System.out.println(">>>>>>>>>>>>>>>@Before>>>>>>>>>>>>>>>>");

    }

/*-------------------------------------11111------------------------------------------------------------*/
    /**
     * 前置通知：在连接点之前执行的通知
     * 前置方法，在目标方法执行前触发
     * @param joinPoint
     * @throws Throwable
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求，记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
        System.out.println(">>>>>>>>>>>>>>>@Before>>>>>>>>>>>>>>>>");


    }

    //环绕通知，可以说是使用最频繁的方式，会将整个方法包围起来
    @Around(value = "pointCut()")
    public Object round(ProceedingJoinPoint joinPoint){
        System.out.println(">>>>>>>>>>>>>>>@Round begin>>>>>>>>>>>>>>>>");
        Object obj = null;
        try {
            obj = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println(">>>>>>>>>>>>>>>@Round end>>>>>>>>>>>>>>>>");
        return obj;
    }



    /**
     * 后置返回通知
     * 这里需要注意的是:
     *      如果参数中的第一个参数为JoinPoint，则第二个参数为返回值的信息
     *      如果参数中的第一个参数不为JoinPoint，则第一个参数为returning中对应的参数
     *       returning：限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知，否则不执行，
     *       对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
     *       argNames  在没有调试信息的情况下编译时，或者在运行时解释切入点时，
     *       通知声明中使用的任何参数的名称都不可用。
     *       仅在这些情况下，有必要在中提供arg名称
     *       注释-这些必须与注释方法中使用的名称重复。
     *       格式是一个简单的逗号分隔列表。
     *       value         应该是方法的路径
     *       pointcut      将通知绑定到的切入点表达式在指定时重写“value”
     *       returning 通知签名中要将返回值绑定到的参数的名称
     *       当目标函数没有返回值时，returnVal将返回null，必须通过returning = “returnVal”
     *       注明参数的名称而且必须与通知函数的参数名称相同
     *       请注意，在任何通知中这些参数都是可选的，需要使用时直接填写即可，不需要使用时，可以完成不用声明出来
     */

    @After(value = "pointCut()")
    public void after(){
        System.out.println(">>>>>>>>>>>@After>>>>>>>>>>>>>>>>>>>>");
    }


    //后置通知，在将返回值返回时执行
    @AfterReturning( value = "pointCut()",returning = "name")
    public void doAfterReturning(Object name) throws Throwable {
        // 处理完请求，返回内容
        logger.info("RESPONSE : " + name);
        System.out.println(">>>>>>>>>>>>>>>@AfterReturning>>>>>>>>>>>>>>>>"+name);
    }

    /**
     * 后置异常通知
     *  定义一个名字，该名字用于匹配通知实现方法的一个参数名，当目标方法抛出异常返回后，将把目标方法抛出的异常传给通知方法；
     *  throwing:限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知，否则不执行，
     *           对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。
     * @param joinPoint
     * @param exception
     */
    @AfterThrowing(value = "pointCut()" ,throwing = "exception")
    public void doAfterThrowingAdvice(JoinPoint joinPoint,Throwable exception){
        //目标方法名：
        logger.info(joinPoint.getSignature().getName());
        if(exception instanceof NullPointerException){
            logger.info("发生了空指针异常!!!!!");
        }
    }


   /* @AfterReturning(returning = "ret")
    public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){
        logger.info("第一个后置返回通知的返回值："+keys);
    }*/


 /*   *//**
     * 后置通知
     * returnVal,切点方法执行后的返回值
     *//*
    @AfterReturning(value="pointcut()",returning = "returnVal")
    public void AfterReturning(JoinPoint joinPoint,Object returnVal){
        System.out.println("我是后置通知...returnVal+"+returnVal);
    }*/




    /**
     * 总结一下
     *  先进入环绕通知 @Round  end   再到@before  再到 controller 再到@Round  end 最后 @After  @AfterReturning
     *  定义切入点的时候需要一个包含名字和任意参数的签名，还有一个切入点表达式，如execution(public * com.example.aop...(..))
     *
     * 切入点表达式的格式：execution([可见性]返回类型[声明类型].方法名(参数)[异常])
     * 其中[]内的是可选的，其它的还支持通配符的使用：
     * 1) *：匹配所有字符
     * 2) ..：一般用于匹配多个包，多个参数
     * 3) +：表示类及其子类
     * 4)运算符有：&&,||,!
     *
     * 切入点表达式关键词用例：
     * 1）execution：用于匹配子表达式。
     * //匹配com.cjm.model包及其子包中所有类中的所有方法，返回类型任意，方法参数任意
     * @Pointcut(“execution(* com.cjm.model...(..))”)
     * public void before(){}
     *
     * 2）within：用于匹配连接点所在的Java类或者包。
     * //匹配Person类中的所有方法
     * @Pointcut(“within(com.cjm.model.Person)”)
     * public void before(){}
     * //匹配com.cjm包及其子包中所有类中的所有方法
     * @Pointcut(“within(com.cjm..*)”)
     * public void before(){}
     *
     * 3） this：用于向通知方法中传入代理对象的引用。
     * @Before(“before() && this(proxy)”)
     * public void beforeAdvide(JoinPoint point, Object proxy){
     * //处理逻辑
     * }
     *
     * 4）target：用于向通知方法中传入目标对象的引用。
     * @Before(“before() && target(target)
     * public void beforeAdvide(JoinPoint point, Object proxy){
     * //处理逻辑
     * }
     *
     * 5）args：用于将参数传入到通知方法中。
     * @Before(“before() && args(age,username)”)
     * public void beforeAdvide(JoinPoint point, int age, String username){
     * //处理逻辑
     * }
     *
     * 6）@within ：用于匹配在类一级使用了参数确定的注解的类，其所有方法都将被匹配。
     * @Pointcut(“@within(com.cjm.annotation.AdviceAnnotation)”)
     * － 所有被@AdviceAnnotation标注的类都将匹配
     * public void before(){}
     *
     * 7）@target ：和@within的功能类似，但必须要指定注解接口的保留策略为RUNTIME。
     * @Pointcut(“@target(com.cjm.annotation.AdviceAnnotation)”)
     * public void before(){}
     *
     * 8）@args ：传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
     * @Before(“@args(com.cjm.annotation.AdviceAnnotation)”)
     * public void beforeAdvide(JoinPoint point){
     * //处理逻辑
     * }
     *
     * 9）@annotation ：匹配连接点被它参数指定的Annotation注解的方法。也就是说，所有被指定注解标注的方法都将匹配。
     * @Pointcut(“@annotation(com.cjm.annotation.AdviceAnnotation)”)
     * public void before(){}
     *
     * 10）bean：通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
     * @Pointcut(“bean(person)”)
     * public void before(){}
     * ————————————————
     * 版权声明：本文为CSDN博主「May的博客」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。
     * 原文链接：https://blog.csdn.net/lmb55/article/details/82470388
     */
}
